/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.util.jar;

import java.io.FilterInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;

/**
 * The Manifest class is used to maintain Manifest entry names and their
 * associated Attributes. There are main Manifest Attributes as well as
 * per-entry Attributes. For information on the Manifest format, please
 * see the
 * <a href="../../../../technotes/guides/jar/jar.html">
 * Manifest format specification</a>.
 *
 * @author David Connelly
 * @see Attributes
 * @since 1.2
 */
public class Manifest implements Cloneable {

  // manifest main attributes
  private Attributes attr = new Attributes();

  // manifest entries
  private Map<String, Attributes> entries = new HashMap<>();

  /**
   * Constructs a new, empty Manifest.
   */
  public Manifest() {
  }

  /**
   * Constructs a new Manifest from the specified input stream.
   *
   * @param is the input stream containing manifest data
   * @throws IOException if an I/O error has occurred
   */
  public Manifest(InputStream is) throws IOException {
    read(is);
  }

  /**
   * Constructs a new Manifest that is a copy of the specified Manifest.
   *
   * @param man the Manifest to copy
   */
  public Manifest(Manifest man) {
    attr.putAll(man.getMainAttributes());
    entries.putAll(man.getEntries());
  }

  /**
   * Returns the main Attributes for the Manifest.
   *
   * @return the main Attributes for the Manifest
   */
  public Attributes getMainAttributes() {
    return attr;
  }

  /**
   * Returns a Map of the entries contained in this Manifest. Each entry
   * is represented by a String name (key) and associated Attributes (value).
   * The Map permits the {@code null} key, but no entry with a null key is
   * created by {@link #read}, nor is such an entry written by using {@link
   * #write}.
   *
   * @return a Map of the entries contained in this Manifest
   */
  public Map<String, Attributes> getEntries() {
    return entries;
  }

  /**
   * Returns the Attributes for the specified entry name.
   * This method is defined as:
   * <pre>
   *      return (Attributes)getEntries().get(name)
   * </pre>
   * Though {@code null} is a valid {@code name}, when
   * {@code getAttributes(null)} is invoked on a {@code Manifest}
   * obtained from a jar file, {@code null} will be returned.  While jar
   * files themselves do not allow {@code null}-named attributes, it is
   * possible to invoke {@link #getEntries} on a {@code Manifest}, and
   * on that result, invoke {@code put} with a null key and an
   * arbitrary value.  Subsequent invocations of
   * {@code getAttributes(null)} will return the just-{@code put}
   * value.
   * <p>
   * Note that this method does not return the manifest's main attributes;
   * see {@link #getMainAttributes}.
   *
   * @param name entry name
   * @return the Attributes for the specified entry name
   */
  public Attributes getAttributes(String name) {
    return getEntries().get(name);
  }

  /**
   * Clears the main Attributes as well as the entries in this Manifest.
   */
  public void clear() {
    attr.clear();
    entries.clear();
  }

  /**
   * Writes the Manifest to the specified OutputStream.
   * Attributes.Name.MANIFEST_VERSION must be set in
   * MainAttributes prior to invoking this method.
   *
   * @param out the output stream
   * @throws IOException if an I/O error has occurred
   * @see #getMainAttributes
   */
  public void write(OutputStream out) throws IOException {
    DataOutputStream dos = new DataOutputStream(out);
    // Write out the main attributes for the manifest
    attr.writeMain(dos);
    // Now write out the pre-entry attributes
    Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry<String, Attributes> e = it.next();
      StringBuffer buffer = new StringBuffer("Name: ");
      String value = e.getKey();
      if (value != null) {
        byte[] vb = value.getBytes("UTF8");
        value = new String(vb, 0, 0, vb.length);
      }
      buffer.append(value);
      buffer.append("\r\n");
      make72Safe(buffer);
      dos.writeBytes(buffer.toString());
      e.getValue().write(dos);
    }
    dos.flush();
  }

  /**
   * Adds line breaks to enforce a maximum 72 bytes per line.
   */
  static void make72Safe(StringBuffer line) {
    int length = line.length();
    if (length > 72) {
      int index = 70;
      while (index < length - 2) {
        line.insert(index, "\r\n ");
        index += 72;
        length += 3;
      }
    }
    return;
  }

  /**
   * Reads the Manifest from the specified InputStream. The entry
   * names and attributes read will be merged in with the current
   * manifest entries.
   *
   * @param is the input stream
   * @throws IOException if an I/O error has occurred
   */
  public void read(InputStream is) throws IOException {
    // Buffered input stream for reading manifest data
    FastInputStream fis = new FastInputStream(is);
    // Line buffer
    byte[] lbuf = new byte[512];
    // Read the main attributes for the manifest
    attr.read(fis, lbuf);
    // Total number of entries, attributes read
    int ecount = 0, acount = 0;
    // Average size of entry attributes
    int asize = 2;
    // Now parse the manifest entries
    int len;
    String name = null;
    boolean skipEmptyLines = true;
    byte[] lastline = null;

    while ((len = fis.readLine(lbuf)) != -1) {
      if (lbuf[--len] != '\n') {
        throw new IOException("manifest line too long");
      }
      if (len > 0 && lbuf[len - 1] == '\r') {
        --len;
      }
      if (len == 0 && skipEmptyLines) {
        continue;
      }
      skipEmptyLines = false;

      if (name == null) {
        name = parseName(lbuf, len);
        if (name == null) {
          throw new IOException("invalid manifest format");
        }
        if (fis.peek() == ' ') {
          // name is wrapped
          lastline = new byte[len - 6];
          System.arraycopy(lbuf, 6, lastline, 0, len - 6);
          continue;
        }
      } else {
        // continuation line
        byte[] buf = new byte[lastline.length + len - 1];
        System.arraycopy(lastline, 0, buf, 0, lastline.length);
        System.arraycopy(lbuf, 1, buf, lastline.length, len - 1);
        if (fis.peek() == ' ') {
          // name is wrapped
          lastline = buf;
          continue;
        }
        name = new String(buf, 0, buf.length, "UTF8");
        lastline = null;
      }
      Attributes attr = getAttributes(name);
      if (attr == null) {
        attr = new Attributes(asize);
        entries.put(name, attr);
      }
      attr.read(fis, lbuf);
      ecount++;
      acount += attr.size();
      //XXX: Fix for when the average is 0. When it is 0,
      // you get an Attributes object with an initial
      // capacity of 0, which tickles a bug in HashMap.
      asize = Math.max(2, acount / ecount);

      name = null;
      skipEmptyLines = true;
    }
  }

  private String parseName(byte[] lbuf, int len) {
    if (toLower(lbuf[0]) == 'n' && toLower(lbuf[1]) == 'a' &&
        toLower(lbuf[2]) == 'm' && toLower(lbuf[3]) == 'e' &&
        lbuf[4] == ':' && lbuf[5] == ' ') {
      try {
        return new String(lbuf, 6, len - 6, "UTF8");
      } catch (Exception e) {
      }
    }
    return null;
  }

  private int toLower(int c) {
    return (c >= 'A' && c <= 'Z') ? 'a' + (c - 'A') : c;
  }

  /**
   * Returns true if the specified Object is also a Manifest and has
   * the same main Attributes and entries.
   *
   * @param o the object to be compared
   * @return true if the specified Object is also a Manifest and has the same main Attributes and
   * entries
   */
  public boolean equals(Object o) {
    if (o instanceof Manifest) {
      Manifest m = (Manifest) o;
      return attr.equals(m.getMainAttributes()) &&
          entries.equals(m.getEntries());
    } else {
      return false;
    }
  }

  /**
   * Returns the hash code for this Manifest.
   */
  public int hashCode() {
    return attr.hashCode() + entries.hashCode();
  }

  /**
   * Returns a shallow copy of this Manifest.  The shallow copy is
   * implemented as follows:
   * <pre>
   *     public Object clone() { return new Manifest(this); }
   * </pre>
   *
   * @return a shallow copy of this Manifest
   */
  public Object clone() {
    return new Manifest(this);
  }

  /*
   * A fast buffered input stream for parsing manifest files.
   */
  static class FastInputStream extends FilterInputStream {

    private byte buf[];
    private int count = 0;
    private int pos = 0;

    FastInputStream(InputStream in) {
      this(in, 8192);
    }

    FastInputStream(InputStream in, int size) {
      super(in);
      buf = new byte[size];
    }

    public int read() throws IOException {
      if (pos >= count) {
        fill();
        if (pos >= count) {
          return -1;
        }
      }
      return Byte.toUnsignedInt(buf[pos++]);
    }

    public int read(byte[] b, int off, int len) throws IOException {
      int avail = count - pos;
      if (avail <= 0) {
        if (len >= buf.length) {
          return in.read(b, off, len);
        }
        fill();
        avail = count - pos;
        if (avail <= 0) {
          return -1;
        }
      }
      if (len > avail) {
        len = avail;
      }
      System.arraycopy(buf, pos, b, off, len);
      pos += len;
      return len;
    }

    /*
     * Reads 'len' bytes from the input stream, or until an end-of-line
     * is reached. Returns the number of bytes read.
     */
    public int readLine(byte[] b, int off, int len) throws IOException {
      byte[] tbuf = this.buf;
      int total = 0;
      while (total < len) {
        int avail = count - pos;
        if (avail <= 0) {
          fill();
          avail = count - pos;
          if (avail <= 0) {
            return -1;
          }
        }
        int n = len - total;
        if (n > avail) {
          n = avail;
        }
        int tpos = pos;
        int maxpos = tpos + n;
        while (tpos < maxpos && tbuf[tpos++] != '\n') {
          ;
        }
        n = tpos - pos;
        System.arraycopy(tbuf, pos, b, off, n);
        off += n;
        total += n;
        pos = tpos;
        if (tbuf[tpos - 1] == '\n') {
          break;
        }
      }
      return total;
    }

    public byte peek() throws IOException {
      if (pos == count) {
        fill();
      }
      if (pos == count) {
        return -1; // nothing left in buffer
      }
      return buf[pos];
    }

    public int readLine(byte[] b) throws IOException {
      return readLine(b, 0, b.length);
    }

    public long skip(long n) throws IOException {
      if (n <= 0) {
        return 0;
      }
      long avail = count - pos;
      if (avail <= 0) {
        return in.skip(n);
      }
      if (n > avail) {
        n = avail;
      }
      pos += n;
      return n;
    }

    public int available() throws IOException {
      return (count - pos) + in.available();
    }

    public void close() throws IOException {
      if (in != null) {
        in.close();
        in = null;
        buf = null;
      }
    }

    private void fill() throws IOException {
      count = pos = 0;
      int n = in.read(buf, 0, buf.length);
      if (n > 0) {
        count = n;
      }
    }
  }
}
